<?php
// phpSmart - The original method for structuring php scripts
// Copyright (C) 2009 Marc Fache ‹marc@fache.be›
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Marc Fache ‹marc@fache.be›
//

class phpSmart {
  
  private $svnrevision = '$Revision: 25 $' ; // svn revision
  
  private $save_analysis  = true ;
  private $etats  = array() ;
  private $events = array() ;
  private $funcs  = array() ;

  private $tmp_dir     = '' ;
  private $schema_dir  = '' ;
  private $request_var = '' ; // override $_POST or $_GET
  
  private $status_var_name = 'e' ; // var name for bypass status
  private $autorun = true ;
  private $fresh   = true ; // default $this->fresh() return true
  private $movnext = 0 ;
  private $pass = '' ;
  
  private $current_state    = false;
  private $next_event       = false;
  private $events_available = array();
  private $session_enable   = false ; // default

  private static $instance ;
 
  public static function getInstance() {
    if ( !isset(self::$instance) ) {
      self::$instance = new phpSmart();
    }
    return self::$instance;
  }

//  private function __construct() {
  public function __construct() {
    
    // use global temporary dir for default temporary dir
      if ( '' == $this->tmp_dir )
        $this->tmp_dir = $this->cleanDirPath(sys_get_temp_dir());

    // use current dir for default schema dir
      if ( '' == $this->schema_dir )
        $this->schema_dir = $this->cleanDirPath(dirname(__FILE__),false);
    
    // if define another schema directory whith constant
      if ( defined( 'PHPSMART_TMP_DIR' ) )
        $this->tmp_dir   = $this->cleanDirPath( PHPSMART_TMP_DIR );

      if ( defined( 'PHPSMART_SCHEMA_DIR' ) )
        $this->schema_dir = $this->cleanDirPath( PHPSMART_SCHEMA_DIR,false) ;
        
      if ( isset($_SESSION) || session_start() ) {
        $this->session_enable = true ;
      }

    return true;
  }
  
  public function setParms($name,$value) {
    
    // default return value
    $return = false ;
    
    switch($name) {
      case('autorun'):
        if ( true === $value || false === $value ) {
          $this->autorun = $value ;
          $return = true ;
        }
        break;
      case('save_analysis'):
        if ( true === $value || false === $value ) {
          $this->save_analysis = $value ;
          $return = true ;
        }
        break;
      case('fresh'):
        if ( true === $value || false === $value ) {
          $this->fresh = $value ;
          $return = true ;
        }
        break;
      case('request_var'):
        if ( !empty($value) && is_array($value) ) {
          $this->request_var = $value ;
          $return = true ;
        }
        break;
      case('status_var_name'):
        if ( !empty($value) && is_string($value) ) {
          $this->status_var_name = $value ;
          $return = true ;
        }
        break;
      default:
        $this->error( 'parameter_unknown', "parameter unknown \"{$name}\"" );
        break;
    }
    
    return $return ;
  }

  public function cleanDirPath($dir='',$check_writable=1) {

    // remove final "/" character

      $dir = preg_replace("/[\/]+$/",'',$dir) ;
      
    // check validity

      if ( empty($dir) ) {
        $this->error ( 'dir_name_empty', "dir name empty \"{$dir}\".") ;
      } elseif ( !file_exists($dir) ) {
        $this->error ( 'dir_not_exist', "dir not exist \"{$dir}\".") ;
      } elseif ( !is_dir($dir) ) {
        $this->error ( 'dir_is_not_dir', "dir is not dir \"{$dir}\".") ;
      } elseif ( !is_readable($dir) ) {
        $this->error ( 'dir_is_not_readable', "dir is not readable \"{$dir}\".") ;
      } elseif ( 1 == $writable && !is_writable($dir) ) {
        $this->error ( 'dir_is_not_writable', "dir is not writable \"{$dir}\".") ;
      }
      
      return $dir ;
  }

  public function loadFile( $filename='', $autorun='' ) {
    
    // filename cleaning
      $filename = trim( $filename ) ;
    
    // bad if empty
      if ( empty($filename) ) $this->error ( 'filename_empty', 'schema filename is empty.' ) ;
      
    // load schema file for bypass to load() method
      if ( !empty($filename) ) {
        $fullpath = $this->schema_dir . '/' . $filename ;
        if ( is_file( $fullpath ) && is_readable( $fullpath ) ) { 
          $schema = file_get_contents( $fullpath ) ;
          $this->load( $schema, $autorun ) ;
          return true ;
        }
      }
      
    $add_error = '' ;
    if ( '' == $this->schema_dir ) {
      $add_error = "<br />\nUse constant PHPSMART_SCHEMA_DIR before <strong>".__FUNCTION__."()</strong>." ;
    }
    $this->error ( 'no_shema_dir_found', "no schema found in dir <strong>\"{$this->schema_dir}\"</strong>." . $add_error ) ;
  }

  public function load( $schema='', $autorun='' ) {
    
    if ( '' !== $autorun      ) $this->setParms ( 'autorun', $autorun ) ;
    
    if ( empty($schema)       ) $this->error ( 'shema_empty', 'schema empty.' ) ;
    if ( ! is_string($schema) ) $this->error ( 'shema_not_a_string', 'schema is not a string.' ) ;
    
    // reset vars
    
      $this->etats  = array() ;
      $this->funcs  = array() ;
      $this->events = array() ;

    // prepare uniq identification string
    // based a checksum of schema

    $checksum = base64_encode(sprintf("%u\n",crc32($schema))) ;
    $checksum = preg_replace('/[^A-Za-z0-9]/','',$checksum);
    
    if ( 'e' == $this->status_var_name ) {
      $this->status_var_name = $checksum ;
    }

    if ( empty ( $checksum ) ) $this->error ( 'checksum_empty', 'bad checksum of schema.' ) ;
    
    if ( true == $this->save_analysis )
    {
      $tmp_dir   = $this->tmp_dir ;
      $file_name = 'phpSmart_' . $checksum ;
      $fullpath  = $tmp_dir . '/' . $file_name ;
    }
    
    if ( true == $this->save_analysis && file_exists( $fullpath ) )
    {
      
      // load last parsed schema for best performance

      $data = unserialize(file_get_contents($fullpath)) ;
      $this->etats  = $data['etats'] ;
      $this->funcs  = $data['funcs'] ;
      $this->events = $data['events'] ;
      
    }
    else 
    {
      // check schema and save this in
      // filesystem for next call
    
      $raw_schema_in_array = preg_split ( "/[\r\n]/", $schema ) ;

      $this->checkValiditySchema($raw_schema_in_array);
    
      if ( true == $this->save_analysis )
      {
        $data = array (
            'etats'  => $this->etats,
            'funcs'  => $this->funcs,
            'events' => $this->events,
          );
        
        if ( ! file_put_contents($fullpath,serialize($data)) ) {
          $this->error ( 'the_schema_can_not_be_saved', "bad save schema in file \"{$fullpath}\"." ) ;
        }
      }
      
    }
    
    if ( true == $this->autorun ) $this->ring();
  }

  private function checkValiditySchema ($raw_schema_in_array) {
    
    if ( empty($raw_schema_in_array) )     $this->error ( 'raw_schema_empty', 'internal error, argument required ('.__FUNCTION__.')' ) ;
    if ( !is_array($raw_schema_in_array) ) $this->error ( 'raw_schema_not_array', 'internal error, argument is not array ('.__FUNCTION__.')' ) ;
    
    // nous devons afficher le schema et les erreurs eventuelles
    // de manière propre, nous utilisons sprintf.
    
    // default method return
    
      $return = true;
    
    // parcours des lignes du schema, les lignes vides sont ignorées,
    // les lignes inconnues sont ignorées également. Les lignes utiles
    // sont reformatées proprement pour diminuer le nombre d'erreur qui
    // surviennent avec une syntaxe peut soignée.
    
      $next = array('etat') ;
    
      $cnt_etat = 0 ;
      $cnt_line = 0 ;
      foreach ( $raw_schema_in_array as $line ) {
        $cnt_line++ ;
        $line = trim ( $line ) ;
        if ( !empty ( $line ) ) {
          
          if ( preg_match ( "/^ *([^\s]+) *:$/", $line, $matches ) ) {
            
            if ( ! in_array('etat',$next) ) $this->error( 'line_wrong_place', "Line in the wrong place {$cnt_line} \"{$line}\"" );
            $next = array('func') ;

            if ( count($this->etats) > 0 ) $cnt_etat++ ;
            
            $etat_name  = ucfirst ( strtolower ( preg_replace ( "/[^a-z0-9]/i", '-', $matches[1] ) ) ) ;
            $this->etats[$cnt_etat] = $etat_name ;
            
          } elseif ( preg_match ( "/^ *\+ *([a-z]+[^\s]+)/i", $line, $matches ) ) {
            
            if ( ! in_array('func',$next) ) $this->error( 'line_wrong_place', "Line in the wrong place {$cnt_line} \"{$line}\"" );
            $next = array('func','event') ;
            
            $func_name  = preg_replace ( "/[^a-z0-9]/i", '_', $matches[1] ) ;
            $this->funcs[$cnt_etat][] = $func_name ;
            
          } elseif ( preg_match ( "/^ *-> *([^\s]+)[ \t]+([^\s]+)/", $line, $matches ) ) {
            
            if ( ! in_array('event',$next) ) $this->error( 'line_wrong_place', "Line in the wrong place {$cnt_line} \"{$line}\"" );
            $next = array('event','etat') ;
            
            $event_name = strtolower ( preg_replace ( "/[^a-z]/i", '_', $matches[1] ) ) ;
            $etat_name  = ucfirst ( strtolower ( preg_replace ( "/[^a-z0-9]/i", '-', $matches[2] ) ) ) ;
            $this->events[$cnt_etat][$event_name] = $etat_name ;
            
          } elseif ( ! empty($line) ) {
            $this->error('unrecognized_line_in_the_schema', "Schema syntax error line {$cnt_line} \"{$line}\"");
          }
          
        }
      }
      
    // on recherche le nom de l'action la plus longue pour pouvoir
    // aligner proprement les etats qui les suivent
    
      $events_max_len = 0 ;
      foreach ( $this->events as $events ) {
        foreach ( $events as $kev => $vev ) {
          $events_max_len = max ( $events_max_len, strlen($kev) ) ;
        }
      }
      
    // quelques definitions
    
      $output    = array(); // sortie du schema ligne par ligne
      $errors    = array(); // erreurs si il y en a
      $max_len = 0 ;
      $ln      = 0 ;
      
      $show_line_number = false ;
      
    // construction et verification des erreurs du schema
    
      foreach ( $this->etats as $ket => $vet )
      {
        // ajoute une ligne vide esthétique avant chaque nouvel état,
        // au tout premier passage on ne fait rien.
        
        if ( $ln > 0 ) {
          // incrémente le numéro de la ligne
          $ln++ ;
          // si nous désirons avoir un numéro devant chaque ligne on le met dans $ln_disp
          $ln_disp = ''; $show_line_number && $ln_disp = $ln ;
          // on conserve la taille de la chaine maximum qui va être affiché
          $max_len = max ( $max_len, strlen(sprintf("%-3s",$ln_disp)) ) ;
          // on crée la ligne avec le numéro de la ligne (si désiré, sinon vide)
          $out = sprintf("%-3s",$ln_disp) ;
          // on place la ligne dans le tableau de sortie
          $output[$ln] = $out ;
        }
        
        // affiche l'etat
        
          // incrémente le numéro de la ligne
          $ln++ ;
          // si nous désirons avoir un numéro devant chaque ligne on le met dans $ln_disp
          $ln_disp = ''; $show_line_number && $ln_disp = $ln ;
          // on conserve la taille de la chaine maximum qui va être affiché
          // sur base de taille de la ligne non colorisée.
          $max_len = max ( $max_len, strlen(sprintf("%-3s %s:",$ln_disp,$vet)) ) ;
          // on crée la ligne qui va être visible avec la version colorisée
          $out = sprintf("%-3s %s:",$ln_disp,$vet) ;
          // on place la ligne dans le tableau de sortie
          $output[$ln] = $out ;
        
        // affiche les fonctions
        
          // si il n'y a pas de fonction pour l'état courant on
          // en crée une vide pour afficher proprement une erreur dessus
          if ( !isset($this->funcs[$ket]) )
          {
            $this->funcs[$ket][] = '' ;
          }
          
          foreach ( $this->funcs[$ket] as $vf )
          {
            $ln++ ;
            $ln_disp = ''; $show_line_number && $ln_disp = $ln ;
            $max_len = max ( $max_len, strlen(sprintf("%-3s  + %s",$ln_disp,$vf)) ) ;
            $out = sprintf("%-3s   + %s",$ln_disp,$vf) ;
            $output[$ln] = $out ;
            
            if ( empty($vf) ) {
              
              // si la ligne est vide (pas de fonction pour cet etat)
              $errors[$ln] = "Function or method required. Sample: \"+ my_func_to_call\"" ;
              
            } else {
              
              // on verifie si la fonction renseignée existe bien réellement
                if ( ! function_exists($vf) ) {
                  $errors[$ln] = "Function not found, create this function first. You can copy the code below ;-)" ;
                  
                  $action = 'your_action_name_here' ;
                  $action_available = '' ;
                  if ( !empty($this->events[$ket]) ) {
                    reset($this->events[$ket]) ;
                    $each = each($this->events[$ket]) ;
                    $action = $each['key'] ;
                    $action_available = implode(', ',array_keys($this->events[$ket])) ;
                  }
                  
                  $tmp = htmlentities("function {$vf}(\$obj,\$pass) {\n"
                       . "  echo \"phpSmart: succes transit by <strong>\".__FUNCTION__.\"()</strong><br />\\n\" ;\n"
                       . "  \n") ;
                       
                  if ( !empty($action_available) ) {
                    $tmp .= htmlentities("  // SAMPLE USAGE:\n"
                         . "  //\${$action}_event_hidden = \$obj->getNextEvent('{$action}','form') ;\n"
                         . "  //\${$action}_event_url    = \$obj->getNextEvent('{$action}','url') ;\n"
                         . "  //list(\$var,\$value)       = \$obj->getNextEvent('{$action}') ;\n"
                         . "  //\$obj->setNextEvent('{$action}');\n"
                         . "  // EVENTS AVAILABLE: {$action_available}\n") ;
                  }
                  
                  $tmp .= "}\n" ;
                  
                  $sample[] = $tmp ;
                  unset($tmp) ;
                }
              
            }
          }
      
      // affiche les evenements
      
        if ( isset($this->events[$ket]) )
        {
          foreach ( $this->events[$ket] as $kev => $vev )
          {
            $ln++ ;
            $ln_disp = ''; $show_line_number && $ln_disp = $ln ;
            $max_len = max ( $max_len, strlen(sprintf("%-3s   -> %-{$events_max_len}s  %s",$ln_disp,$kev,$vev)) ) ;
            $current_events_max_len = $events_max_len + ( strlen($kev) - strlen($kev) ) ; // retrait
            $out = sprintf("%-3s   -> %-{$current_events_max_len}s  %s",$ln_disp,$kev,$vev) ;
            $output[$ln] = $out ;
            
            // vérifie si l'etat ciblé existe dans le schema
            if ( !in_array($vev,$this->etats) ) {
              $errors[$ln] = "Target unknown" ;
            }
          }
        }
        
    }
    
    if ( empty($this->etats) ) {
      $this->error ( 'no_state_found', 'no state found in schema. State syntax sample: "My-first-status:"' ) ;
    }
    
    if ( !empty($errors) )
    {
      echo "<pre>" ;
      foreach ( $output as $ln => $out ) {
        $err = '' ;
        if ( isset($errors[$ln]) ) $err = '<-- ' .$errors[$ln] ;
        printf ("%-{$max_len}s    %s\n",$out,$err) ;
      }
      if ( !empty($sample) ) {
        echo "\n\n" ;
        echo "Sample of code for your script (copy/paste) :\n" ;
        echo "---------------------------------------------\n\n" ;
        echo implode("\n",$sample) ;
      }
      echo "</pre>" ;
      exit;
    }
    
    return $return ;
  }
  
  public function run() {
    $this->ring();
  }
  
  private function ring() {
    
    if ( empty($this->request_var) ) {
      $formvars = ( !empty($_POST) ) ? $_POST : $_GET ;
    } else {
      $formvars = $this->request_var ; 
    }

    if ( empty($formvars["{$this->status_var_name}"]) ) {
      $eidraw = '0:0' ;
    } else {
      $eidraw = $formvars["{$this->status_var_name}"] ;
    }
      
    list($eid,$mov) = split(':',$eidraw) ;
      
    $this->movnext = $mov + 1 ;
    
    //-------8<---snip-----------
    if ( $this->session_enable ) {
      
      if ( ! isset($_SESSION[$this->status_var_name]['mov']) ) {
        
        $current = $mov + 1 ;
        $next    = $this->movnext + 1 ;
        
      } else {
        
        $current = $_SESSION[$this->status_var_name]['mov']['current'] ;
        $next    = $_SESSION[$this->status_var_name]['mov']['next'] ;
  
        if ( ($mov+1) == $next ) { // fresh
          $this->fresh = true ;
          $current++ ;
          $next++ ;
        } else { // no fresh
          $this->fresh = false ;
        }
      }
      $_SESSION[$this->status_var_name]['mov']['current'] = $current ;
      $_SESSION[$this->status_var_name]['mov']['next']    = $next ;
    }
    //-------8<---snap-----------
      
    $eid = $this->check_etat_exist ( $eid ) ;
    
    // on sauve l'etat courant ;
    $this->current_state = $eid ;
    
    $etats_flip = array_flip($this->etats) ;
      
    /* Ring... */
    do {
      
      $this->events_available = array();

      /* On cree les globales des evenements possibles pour l'etat courant */
      if ( !empty($this->events[$eid]) )
        foreach ( $this->events[$eid] as $k => $v ) {
          $this->events_available[$k]  = $etats_flip[$this->events[$eid][$k]] ;
          $last_event_key[] = "{$k}_event" ;
        }

      $this->next_event = false ;

      /* On execute les fonctions */
      foreach ( $this->funcs[$eid] as $to_execute ) {
        if ( ! function_exists($to_execute) ) $this->error ( 'func_not_found', "next function <strong>{$to_execute}()</strong> not found." ) ;
        $to_execute($this,$this->pass);
      }

      $last_eid = $eid ;
      $eid = $this->next_event ;

    } while ( false !== $eid ) ;
    
  }
  
  public function __toString() {
    $debug_backtrace = debug_backtrace();
    if ( isset($debug_backtrace[1]['function']) && !empty($debug_backtrace[1]['function']) )
    {
      $func_called = $debug_backtrace[1]['function'] ;
      if ( function_exists( $func_called ) ) {
        $this->error ( 'func_can_not_be_called', "{$func_called}() function can not be called by phpSmart. Bad usage." ) ;
      }
    }
    $return = "phpSmart be not converted to string. Process halted." ;
    echo $return ;
    return $return ;
  }

  private function check_etat_exist ( $eid ) {
    $etat_txt = $this->etats[$eid] ;
    if ( empty($etat_txt) ) return '0' ; // status received not exist
    return $eid ;
  }

  private function event_exist ( $event ) {
    return ( isset ( $this->events_available[$event] ) ) ? true : false ;
  }
  
  public function getNextEvent ( $event, $format='' ) {
    if ( !$this->event_exist($event) ) {
      $this->error ( 'event_unknown', "unknown event <strong>\"{$event}\"</strong>." ) ;
    }
    
    $var_name = $this->status_var_name ;
    $value    = $this->events_available[$event] ;
    
    $value .= ':' . $this->movnext ;
    
    if ( 'form' == $format )
      return "<input type=\"hidden\" name=\"{$var_name}\" value=\"{$value}\" />" ;
    if ( 'url' == $format )
      return "{$var_name}={$value}" ;
    else
      return array($var_name,$value) ;
  }

  public function setNextEvent ( $event, $pass='' ) {
    if ( !$this->event_exist($event) ) {
      $this->error ( 'event_unknown', "unknown event <strong>\"{$event}\"</strong>." ) ;
    }
    $this->pass = $pass ;
    $this->next_event = $this->events_available[$event] ;
  }
  
  public function fresh() {
    return $this->fresh ;
  }
  
  public function reset() {
    if ( $this->session_enable ) {
      $_SESSION[$this->status_var_name] = array();
      $this->fresh = true ;
    }
  }
  
  private function error($err,$msg='') {
    
    $dbg = debug_backtrace();
    $line = $dbg['0']['line'] ;
    $func = $dbg['1']['function'] ;
    
    if ( preg_match("/ ([\d]+) /",$this->svnrevision,$matches) ) {
      $revision = $matches[1] ;
    }
    
    $e_array = array(
        'rev'  => $revision,
        'func' => $func,
        'err'  => $err
      );
      
    $e = rawurlencode(implode(':',$e_array)) ;
    
    $error = "(<a target=\"_blank\" href=\"http://www.phpsmart.net/errors.php?e={$e}\">help</a>) phpSmart ({$func}()/{$line}): {$msg}<br />\n" ;
    exit ( $error ) ;
  }
    
}
